package bman.game.battletank;

import static bman.game.engine.Entity.*;
import static bman.game.engine.GameConsole.*;

import java.awt.Dimension;
import java.awt.geom.Rectangle2D;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import bman.game.engine.Entity;
import bman.game.engine.Game;
import bman.game.engine.GameConsole;
import bman.game.engine.GameMap;
import bman.game.engine.GraphicsEngine;
import bman.game.engine.ImageLibrary;
import bman.game.engine.TextFeed;
import bman.tools.net.Radio;

public class BattleTank extends Game
{
	Logger log = Logger.getLogger(getClass().getName());
	
	public static final int WIDTH = 500;   
    public static final int HEIGHT = 400;
	
	public static final String NORTH = "NORTH";
	public static final String SOUTH = "SOUTH";
	public static final String EAST = "EAST";
	public static final String WEST = "WEST";
	
	private Entity northWall;
	private Entity southWall;
	private Entity westWall;
	private Entity eastWall;
	
	static final int padding = 30;
	static final int MOVESPEED = 10;
	
	/**
	 * ACTIONS
	 */
	public static final String JOIN = "JOIN";
	public static final String DETAIL = "DETAIL";
	
	Entity entity;

	int positionTextIndex = -1;
	int lifeTextIndex = -1;
	
	boolean remotelyInitialized = false;

	static
	{
		
		/*
		 * Load images
		 */
		String[] images = {
				"bg.gif",
				"biggest.gif",
				"bluetank.gif",
				"bluetankEAST.gif",
				"bluetankNORTH.gif",
				"bluetankSOUTH.gif",
				"bluetankWEST.gif",
				"brick.gif",
				"brickcrack.gif",
				"bullet.gif",
				"bulletEAST.gif",
				"bulletNORTH.gif",
				"bulletSOUTH.gif",
				"bush.gif",
				"explode.gif",
				"grass.gif",
				"greentank.gif",
				"greentankdead.gif",
				"greentankEAST.gif",
				"greentankNORTH.gif",
				"greentankSOUTH.gif",
				"greentankWEST.gif",
				"pinktank.gif",
				"pinktankEAST.gif",
				"pinktankNORTH.gif",
				"pinktankSOUTH.gif",
				"pinktankWEST.gif",
				"redtank.gif",
				"redtankEAST.gif",
				"redtankNORTH.gif",
				"redtankSOUTH.gif",
				"redtankWEST.gif",
				"smiling.gif",
				"stars.gif",
				"steelblock.gif",
				"steelblock2.gif",
				"steeltiled.gif",
				"wall.gif",
				"water.gif",
				"yellowtank.gif",
				"yellowtankEAST.gif",
				"yellowtankNORTH.gif",
				"yellowtankSOUTH.gif",
				"yellowtankWEST.gif",
				};
		
		for(int i = 0; i < images.length;i++)
		{
			loadImage(images[i]);
		}
	}
	
	public BattleTank()
	{
		GarbageCollector explosionManager = new GarbageCollector(this.getMap());
		this.getMap().setSize(WIDTH,HEIGHT);
		initWalls();
	}
	
	/**
	 * A convinience method to load images into the image ibrary.
	 * The path to the image location is hardcoded as "/images" and the image
	 * name is appended to this path. The key will be the uppercase of the image
	 * name less the file extansion. For example for an image name "greentank.gif"
	 * the image key shall be GREENTANK and the image to be loaded will be
	 * "/images/greentank.gif"
	 * @param imageName
	 */
	private static void loadImage(String imageFileName)
	{
		int dot = imageFileName.indexOf('.');
		String name = imageFileName.substring(0, dot);
		ImageLibrary.loadImage(name.toUpperCase(), "/images/"+imageFileName);
	}
	
	public void init()
	{
		GameMap map = getMap();

		InputStream  i = this.getClass().getResourceAsStream("/conf/default.map");
		boolean load = map.load(new InputStreamReader(i));
		if(load == false)
		{
			throw new RuntimeException("Unable to load BattleTank map");			
		}

		map.setSize(WIDTH, HEIGHT);
		initWalls();
	}
	
	public void initWalls()
	{
		GameMap map = getMap();
		/*
		 * Create the walls
		 */
		int w = map.width();
		int h = map.height();
		
		int wallThickness = 1000;
		
		northWall = map.createEntity("WALL",0,-wallThickness,0);
		northWall.width(w);
		northWall.height(wallThickness);
		northWall.movable(false);
		
		southWall = map.createEntity("WALL",0,h,0);
		southWall.width(w);
		southWall.height(wallThickness);
		southWall.movable(false);
		
		westWall = map.createEntity("WALL",0,0,0);
		westWall.width(1);
		westWall.height(h);
		westWall.movable(false);
		
		eastWall = map.createEntity("WALL",w,0,0);
		eastWall.width(1);
		eastWall.height(h);
		eastWall.movable(false);
	}
	
	public void displayEntityStats()
	{
		positionTextIndex = getTextFeed().setText(positionTextIndex, "Enity Position: "+entity.x() +", " + entity.y());
		lifeTextIndex = getTextFeed().setText(lifeTextIndex,"Life: "+entity.life());
	}
	
	public Dimension getSize()
	{
		return new Dimension(WIDTH,HEIGHT);
	}
	
	/**
	 * Performs an action and the owner of the action is the Entity that
	 * is passed as a parameter.
	 * 
	 * This method returns a list of commands that needs to be broadcasted.
	 * This list of commands are the result of the action that was performed.
	 * 
	 * If nothing needs to be broadcasted an emptly List will be returned. This method
	 * should never return null
	 */
	public List<String> process(String action)
	{
		if(action.contains(JOIN))
		{
			String color = GameConsole.getField(action,"COLOR");
			this.log.info("Joining with tank color = "+color);
			entity = getMap().addEntity(color+"tank",200,200,1);
			entity.life(100);
			collisions(entity);
		}
				
		return process(action,entity);
	}
	
	/**
	 * Performs an action and the owner of the action is the Entity that
	 * is passed as a parameter.
	 * 
	 * This method returns a list of commands that needs to be broadcasted.
	 * This list of commands are the result of the action that was performed.
	 * 
	 * If nothing needs to be broadcasted an emptly List will be returned. This method
	 * should never return null
	 */
	public List<String> process(String action, Entity entity)
	{
		GameMap map = getMap();
		
		/*
		 * The list of commands that needs to be broadcasted.
		 * These list of commands are the result of the 
		 * action that was performed.
		 */
		List responseList = new ArrayList();
		
		String a = action.toUpperCase();
		log.info("performAction: "+a);
		
		if(a.contains("DISAPPEAR"))
		{
			map.remove(entity);
		}
		
		if(a.contains("MOVE"))
		{
			if(entity.life() <= 0)
			{
				log.info("Player tank is dead. Cannot move it");
			}
			else
			{
				move(entity,a);
				log.info("Position after movement: "+entity);
			}
		}
		
		if(a.contains("FIRE"))
		{
			if(FireLimitter.canFire(entity) == false)
			{
				log.fine("Entity ID: "+entity.getID()+". Fire suppressed.");
				return responseList;
			}

			responseList.add("FIRE ID:"+entity.getID());
		
			int x = 0;
			int y = 0;
			
			if(NORTH.equalsIgnoreCase(entity.getBearing()))
			{
				x = entity.x() + entity.width() /2;
				y = entity.y() - padding;
			}
			else if(SOUTH.equalsIgnoreCase(entity.getBearing()))
			{
				x = entity.x() + entity.width() /2;
				y = entity.y() + entity.height() + padding;
			}
			else if(EAST.equalsIgnoreCase(entity.getBearing()))
			{
				x = entity.x() + entity.width() + padding;
				y = entity.y() + entity.height() /2;
			}
			else if(WEST.equalsIgnoreCase(entity.getBearing()))
			{
				x = entity.x() - padding;
				y = entity.y() + entity.height() /2;
			}
			
			Entity e = map.addEntity("bullet",x,y,entity.z());
			e.setBearing(entity.getBearing());
			generateExplosion(map,entity);
			
			collisions(e);
			new ProjectileAnimator(e,this);
			
			
		}
		
		displayEntityStats();
		
		/*
		 * Make a list of entities that has changed.
		 */
		List entities = map.getEntities();
		for(int i=0;i<entities.size();i++)
		{
			Entity e = (Entity)entities.get(i);
			if(e.hasChanges() 
				&& !e.getType().equalsIgnoreCase(Types.BULLET) 
				&& !e.getType().equalsIgnoreCase(Types.EXPLOSION))
			{
				responseList.add(e.getChanges());
			}
		}
		
		return responseList;
	}
	
	/**
	 * Generates an explosion in the map. The position of the explosion is computed 
	 * based on the position of the entity that is passes a paratere to this method.
	 * 
	 */
	public void generateExplosion(GameMap map, Entity entity)
	{
		new ExplosionAntimator(map,entity);
	}
	
	public void move(Entity entity, String direction)
	{
		long movement = 5;
		
		int x = entity.x();
		int y = entity.y();
		
		if(isBullet(entity))
		{
			movement = movement*3;
		}
		
		if(direction.contains(NORTH))
		{
			y -= movement;
			entity.setBearing(NORTH);
		}
		
		if(direction.contains(SOUTH))
		{
			y += movement;
			entity.setBearing(SOUTH);
		}
		
		if(direction.contains(EAST))
		{
			x += movement;
			entity.setBearing(EAST);
		}
		
		if(direction.contains(WEST))
		{
			x -= movement;
			entity.setBearing(WEST);
		}
		
		entity.updatePosition(x,y);
		
		collisions(entity);
	}
	
	public boolean hasReachedBorder(int x, int y, int width, int height)
	{
		GameMap map = this.getMap();
		return (x < 0 || y < 0 || (x + width- map.width()) > 0 || (y +height- map.height()) > 0 );
	}
	
	public void collisions(Entity bumper)
	{
		collisions(bumper,0);
	}
	
	/**
	 * This method checks if the entity passed in has collided with other entities in the map.
	 * When an entity collides with another the following rule is applied:
	 * <p>
	 * <ul>
	 * <li> The entity that has collided shall recoil </li>
	 * <li> If the entity collides with another movable entity. Both entity shall recoil </li>
	 * <li> If the entity collides with a bullet. There will be an explosion and the bullet shall disappear </li>
	 * <li> After an entity recoils, it will be checked if it collided with another entity. This is called recoil
	 * collision. Recoil collision can happen 10 times at most. </li>
	 * </ul>
	 * @param level - this is the n'th number that this method has been called. I put it here to
	 * avoid stock overflow. 
	 */
	private void collisions(Entity bumper, int level)
	{
		log.finest("collision level "+level);
		/*
		 * This method calls itself. Here we limit how many times it 
		 * can call itself. 
		 */
		if(level > 10)
		{
			return;
		}
		else
		{
			level++;
		}
		
		GameMap map = this.getMap();
		List miList = new ArrayList(map.getEntiteis(bumper.z()));
		
		miList.add(northWall);
		miList.add(southWall);
		miList.add(westWall);
		miList.add(eastWall);
		
		for(int i = 0; i<miList.size(); i++)
		{
			Entity bumped = (Entity)miList.get(i);
			
			if(!bumped.equals(bumper))
			{
				Rectangle2D r1 = bumper.getShape().getBounds2D();
				Rectangle2D r2 = bumped.getShape().getBounds2D();
				
				boolean hasCollided = r2.intersects(r1);
				if(hasCollided)
				{
					log.fine("Collision level +:"+level+"!! \n"+
							"bumped: "+bumped.toString()+"\n"+
							"bumper: "+bumper.toString());
							
					if(isBullet(bumper))
					{
						log.fine("Bullet hit: "+bumped.toString());
						generateExplosion(map,bumper);
						map.remove(bumper);
						bumper.deactivate();
						bumper.movable(false);
						
						if(level == 1)
						{
							bumped.life(bumped.life()-10);
							if(bumped.life() < 0)
							{
								if(Types.BRICK.equalsIgnoreCase(bumped.getType()))
								{
									bumped.setType(Types.BRICK_DEAD);
									bumped.z(bumped.z()-1);
								}
								else
								{
									bumped.setType(Types.GREENTANK_DEAD);
								}
							}
						
						}
						
						if(bumped.movable())
						{
							int x = bumped.x();
							int y = bumped.y(); 
							if(NORTH.equalsIgnoreCase(bumper.getBearing()))
							{
								y -= MOVESPEED*2;
							}
							else if(SOUTH.equalsIgnoreCase(bumper.getBearing()))
							{
								y += MOVESPEED*2;
							}
							else if(WEST.equalsIgnoreCase(bumper.getBearing()))
							{
								x -= MOVESPEED*2;
							}
							else if(EAST.equalsIgnoreCase(bumper.getBearing()))
							{
								x += MOVESPEED*2;
							}
							bumped.updatePosition(x,y);
						}
					}
					
					int bumped_X = bumped.x();
					int bumped_Y = bumped.y();
					
					int bumper_x = bumper.x();
					int bumper_y = bumper.y();
					
					boolean bumped_movable = bumped.movable();
					
					/*
					 * bumped <-- bumper
					 */
					if(bumped_X <= bumper_x)
					{
						bumper_x += MOVESPEED;
						
					    if(bumped_movable)
					    	bumped_X -= MOVESPEED;
					}
					
					/*
					 * bumper --> bumped
					 */
					else if(bumped_X >= bumper_x)
					{
						bumper_x -= MOVESPEED;
						
						if(bumped_movable)
							bumped_X += MOVESPEED;
					}
					
					/*
					 * bumper
					 *   |
					 * bumped  
					 */
					if(bumped_Y > bumper_y)
					{
						bumper_y = bumper_y - MOVESPEED;
						
						if(bumped_movable)
					    	bumped_Y += MOVESPEED;
					}
					/*
					 * bumped
					 *   |
					 * bumper  
					 */
					else if(bumped_Y <= bumper_y)
					{
						bumper_y = bumper_y + MOVESPEED;
						
						if(bumped_movable)
							bumped_Y -= MOVESPEED;
					}
					
					bumper.updatePosition(bumper_x, bumper_y);
					bumped.updatePosition(bumped_X, bumped_Y);
					
					log.fine("Modified values (collision level "+level+":\n"+
							"bumped: "+bumped.toString()+"\n"+
							"bumper: "+bumper.toString());

					
					collisions(bumper,level);
					
					if(bumped_movable)
					{
						collisions(bumped,level);
						keepInside(bumped);
					}
					
					keepInside(bumper);
	
				}
			}
		}
		
		log.fine("Modified values before exiting collision(level "+level+":\n"+
				"bumper: "+bumper.toString());
	}
	
	private void keepInside(Entity bumper)
	{
		if(bumper.y() < northWall.y())
		{
			log.fine("north wall is hit");
			bumper.y(northWall.y()+MOVESPEED);
		}
		
		if(bumper.y() + bumper.height() > southWall.y())
		{
			log.fine("south wall is hit");
			bumper.y(southWall.y() - bumper.height() - MOVESPEED);
		}

		if(bumper.x() + bumper.width() > eastWall.x())
		{
			log.fine("east wall is hit");
			bumper.x(eastWall.x() - bumper.width() - MOVESPEED);
		}
		
		if(bumper.x() < westWall.x()+westWall.width())
		{
			bumper.x(westWall.x()+westWall.width()+MOVESPEED);
		}
		
	}
	
	/**
	 * Message parsing and interpretation shall be as follows in order:<p>
	 * <ol>
	 * <li>If the message contains INIT (case insensitive), GameConsole 
	 *                   will send Entity Details of the entire map.</li>
	 * <li>Message starting with ID is interpreted as Entity Details</li>
	 * 
	 * </ol>
	 */
	public List<String> processRemote(String s)
	{
		GameMap map = getMap();
		log.info("Command recieved: "+s);
		
		List<String> commandList = new ArrayList<String>();
		
		if(s.toUpperCase().contains("INIT"))
		{
			log.info("Sending Map...");
			
			List l = map.getEntities();
			for(int i = 0;i < l.size();i++)
			{
				Entity e = (Entity)l.get(i);
				commandList.add(e.toString());
			}
		}
		else if(s.contains("CONSOLE MESSAGE: WELCOME"))
		{
			commandList.add(this.entity.toString());
			
		}
		else if(s.contains(DETAIL) )
		{
			String id = getField(s,ID);
			Entity e = map.getEntity(id);
			commandList.add(e.toString());
		}
		else if(s.contains("FIRE"))
		{
			String id = getField(s,"ID");
			Entity e = map.getEntity(id);
			process(s,e);
		}
		else if(s.startsWith("ID"))
		{
			String id = getField(s,ID);
			
			if(id == null)
			{
				log.warning("BattleTank recieved an entity with a null ID. This transmission shall be ignored.");
				return commandList;
			}
			
			String x = getField(s,X);
			String y = getField(s,Y);
			String z = getField(s,Z);
			String bearing = getField(s,BEARING);
			String type = getField(s,TYPE);
			String active = getField(s,ACTIVE);
			String movable = getField(s,MOVABLE);
			
			Entity e = map.getEntity(id);
			
			if(e == null)
			{
				/*
				 * If entity type is null it means that this entity
				 * is existing on other servers and we are only
				 * getting the changes. We need send a request 
				 * for its full details
				 */
				if(type == null)
				{
					commandList.add(DETAIL+" ID:"+id);
					return commandList;
				}
				
				log.info("Creating entity type: "+type);
				e = map.createEntity(type,0,0,0);
				e.setID(id);
				map.add(e);
				
			}
			
			if(x != null)
				e.x(Integer.parseInt(x));
			
			if(y != null)
				e.y(Integer.parseInt(y));
			
			if(z != null)
				e.z(Integer.parseInt(z));
			
			if(type != null)
				e.setType(type);

			if(bearing != null)
				e.setBearing(bearing);
			
			if(movable != null)
				e.movable(Boolean.parseBoolean(movable));
			
			if(active != null)
				map.remove(e);
			
			/*
			 * Delete changes of the entity
			 */
			e.getChanges();
		}
		else
		{
			log.warning("GameMape does not recognized the messaged received: "+s);
		}
		
		return commandList;
	}
	

	public static boolean isBullet(Entity e)
	{
		return e.getType().toUpperCase().contains("BULLET");
	}
	
	static class Types
	{
		public static final String BULLET = "BULLET";
		public static final String BRICK = "BRICK";
		public static final String BRICK_DEAD = "brickcrack";
		public static final String EXPLOSION = "explode";
		
		public static final String GREENTANK = "greentank";
		public static final String GREENTANK_DEAD = "greentankdead";
	}

}
